/*
 * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2006.
 *
 * Licensed under the Aduna BSD-style license.
 */
 
package edu.tju.commons.xml;

import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;

import org.w3c.dom.Attr;
import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.DocumentFragment;
import org.w3c.dom.DocumentType;
import org.w3c.dom.Element;
import org.w3c.dom.Entity;
import org.w3c.dom.EntityReference;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Notation;
import org.w3c.dom.ProcessingInstruction;
import org.w3c.dom.Text;

/**
 * Defines some util methods on DOM nodes.
 */
public class NodeUtil {

	/**
	 * Get all child elements of the given node.
	 *
	 * @param node
	 *        the node to get the childelements from.
	 * @return all childnodes that are element nodes.
	 */
	public static List<Element> getChildElements(Node node) {
		ArrayList<Element> result = new ArrayList<Element>();
		// Loop over all childnodes:
		NodeList childNodes = node.getChildNodes();
		int length = childNodes.getLength();
		for (int i = 0; i < length; i++) {
			Node next = childNodes.item(i);
			// If it is an element, add it to the result list:
			if (next.getNodeType() == Node.ELEMENT_NODE) {
				result.add((Element) next);
			}
		}
		return result;
	}

	/**
	 * Get all descending elements from the given node with the given name.
	 *
	 * @param node
	 *        the parent node
	 * @param name
	 *        the tagname of the elements that are returned
	 * @return A List of Element's
	 */
	public static List<Element> getDescendingElements(Node node, String name) {
		ArrayList<Element> result = new ArrayList<Element>();
		getDescendingElements(node, name, result);
		return result;
	}

	private static void getDescendingElements(Node node, String name, ArrayList<Element> result) {
		List<Element> allChildren = getChildElements(node);
		int nofChildren = allChildren.size();
		for (int i = 0; i < nofChildren; i++) {
			Element child = (Element) allChildren.get(i);
			if (child.getTagName().equals(name)) {
				result.add(child);
			}
			// do recursion
			getDescendingElements(child, name, result);
		}
	}

	/**
	 * Get all descending nodes from the given node with the given name.
	 *
	 * @param node
	 *        the node to get the descending nodes from.
	 * @param name
	 *        the name of the nodes that are returned
	 * @return all descending nodes with the correct name.
	 */
	public static List<Node> getDescendingNodes(Node node, String name) {
		List<Node> wantedChildren;
		NodeList allChildren;
		Node child;
		ArrayList<Node> result = new ArrayList<Node>();
		// first, get the correctly named children of this node and add
		// them to our result.
		wantedChildren = getChildNodes(node, name);
		result.addAll(wantedChildren);
		// then, get _all_ children of this node and iterate over them
		allChildren = node.getChildNodes();
		for (int i = 0; i < allChildren.getLength(); i++) {
			child = allChildren.item(i);
			// recursively add all correctly named nodes to the result
			// list.
			result.addAll(getDescendingNodes(child, name));
		}
		return result;
	}

	public static List<Node> getChildNodes(Node node, String name) {
		ArrayList<Node> result = new ArrayList<Node>();
		// Loop over all childnodes:
		NodeList childNodes = node.getChildNodes();
		int length = childNodes.getLength();
		for (int i = 0; i < length; i++) {
			Node next = childNodes.item(i);
			// If it is an element and it has that name, add it to
			// the result list:
			if (next.getNodeName().equals(name)) {
				result.add(next);
			}
		}
		return result;
	}

	/**
	 * Get all childELEMENTS with name 'name' from the given node.
	 *
	 * @param node
	 *        the node to get the childelements from.
	 * @return all childnodes that are an elementnode.
	 */
	public static List<Element> getChildElements(Node node, String name) {
		ArrayList<Element> result = new ArrayList<Element>();
		// Loop over all childnodes:
		NodeList childNodes = node.getChildNodes();
		int length = childNodes.getLength();
		for (int i = 0; i < length; i++) {
			Node next = childNodes.item(i);
			// If it is an element and it has that name, add it to
			// the result list:
			if (next.getNodeType() == Node.ELEMENT_NODE && next.getNodeName().equals(name)) {
				result.add((Element) next);
			}
		}
		return result;
	}

	/**
	 * Returns the first child element with the given name. Returns 'null' when
	 * there is no such element.
	 */
	public static Element getChildElement(Node node, String name) {
		Node child;
		// Loop over all childnodes:
		NodeList childNodes = node.getChildNodes();
		int length = childNodes.getLength();
		for (int i = 0; i < length; i++) {
			// fetch the next child
			child = childNodes.item(i);
			// If it is an element and it has that name: return it
			if (child.getNodeType() == Node.ELEMENT_NODE && child.getNodeName().equals(name)) {
				return (Element) child;
			}
		}
		// element not found
		return null;
	}

	/**
	 * Returns the n-th child element. Returns 'null' when there is no such
	 * element.
	 *
	 * @throws IllegalArgumentException
	 *         when index < 0
	 */
	public static Element getChildElement(Node node, int index) {
		if (index < 0) {
			throw new IllegalArgumentException("index should be > 0");
		}
		List<Element> childElements = getChildElements(node);
		if (index >= childElements.size()) {
			return null;
		}
		else {
			return childElements.get(index);
		}
	}

	/**
	 * Returns the last child element of the specified node. Returns 'null' when
	 * the node has no child elements.
	 */
	public static Element getLastChildElement(Node node) {
		Node child;
		// loop over all child nodes
		NodeList childNodes = node.getChildNodes();
		for (int i = childNodes.getLength() - 1; i >= 0; i--) {
			// fetch the next child
			child = childNodes.item(i);
			// If it is an element and it has that name: return it
			if (child.getNodeType() == Node.ELEMENT_NODE) {
				return (Element) child;
			}
		}
		// element not found
		return null;
	}

	/**
	 * Get all text-content of a node. All child-elements are recursively
	 * searched for TextNodes. The data of the textnodes is added to the result
	 * string.
	 *
	 * @param node
	 *        the node to get the text from.
	 * @return result a string containing all text of the node with the markup
	 *         removed.
	 */
	public static String getText(Node node) {
		StringBuilder buffer = new StringBuilder(100);
		getText(node, buffer, false);
		return buffer.toString();
	}

	public static String getText(Node node, boolean spaces) {
		StringBuilder buffer = new StringBuilder(100);
		getText(node, buffer, spaces);
		return buffer.toString();
	}

	/**
	 * Add all text-content of a node to a stringBuilder. All childelements are
	 * recursively searched for TextNodes.
	 *
	 * @param node
	 *        the node to get the text from.
	 * @param buffer
	 *        a stringbuilder to add the text to.
	 * @param spaces
	 *        indicates whether to put spaces between the TEXT nodes, before
	 *        concatenating their content. This is useful if you want to know
	 *        where words begin and end in your original text.
	 */
	public static void getText(Node node, StringBuilder buffer, boolean spaces) {
		switch (node.getNodeType()) {
			case Node.CDATA_SECTION_NODE:
			case Node.TEXT_NODE: {
				buffer.append(((Text) node).getData());
				if (spaces) {
					buffer.append(" ");
				}
				break;
			}
				// For elements and document traverse the childnodes:
			case Node.ELEMENT_NODE:
			case Node.DOCUMENT_NODE: {
				// traverse all childnodes:
				NodeList childNodes = node.getChildNodes();
				int length = childNodes.getLength();
				for (int i = 0; i < length; i++) {
					getText(childNodes.item(i), buffer, spaces);
				}
				break;
			}
		}
	}

	public static String toLegalXMLString(String s) {
		// we need to find out how to do this, which characters are
		// valid xml and which characters are not. If they are not,
		// how do we escape 'em? unicode entities or something like
		// that?
		return s;
	}

	/**
	 * Write the contents of a DOM (sub)tree to a stream. This method puts no
	 * extra whitespace in the document, desired whitespace should instead be
	 * present in the given node (perhaps this might become an option in the
	 * future).
	 */
	public static void writeXML(Node node, OutputStream output) {
		PrintStream stream = new PrintStream(output);
		writeXMLNode(node, stream);
	}

	public static void writeXMLNode(Node node, PrintStream output) {
		short type = node.getNodeType();
		switch (type) {
			case (Node.DOCUMENT_NODE): {
				writeDocument(((Document) node), output);
				break;
			}
			case (Node.DOCUMENT_TYPE_NODE): {
				writeDocumentType(((DocumentType) node), output);
				break;
			}
			case (Node.DOCUMENT_FRAGMENT_NODE): {
				writeDocumentFragment(((DocumentFragment) node), output);
				break;
			}
			case (Node.ELEMENT_NODE): {
				writeElement(((Element) node), output);
				break;
			}
			case (Node.ATTRIBUTE_NODE): {
				writeAttribute(((Attr) node), output);
				break;
			}
			case (Node.TEXT_NODE): {
				writeText(((Text) node), output);
				break;
			}
			case (Node.CDATA_SECTION_NODE): {
				writeCDATA(((CDATASection) node), output);
				break;
			}
			case (Node.ENTITY_NODE): {
				writeEntity(((Entity) node), output);
				break;
			}
			case (Node.ENTITY_REFERENCE_NODE): {
				writeEntityReference(((EntityReference) node), output);
				break;
			}
			case (Node.PROCESSING_INSTRUCTION_NODE): {
				writeProcessingInstruction(((ProcessingInstruction) node), output);
				break;
			}
			case (Node.COMMENT_NODE): {
				writeComment(((Comment) node), output);
				break;
			}
			case (Node.NOTATION_NODE): {
				writeNotation(((Notation) node), output);
				break;
			}
		}
	}

	public static void writeDocument(Document document, PrintStream output) {
		// print XML header
		output.print("<?xml version=\"1.0\"?>");
		// handle documentType and whitespace around it
		writeChildren(document, output);
	}

	public static void writeDocumentType(DocumentType documentType, PrintStream output) {
		output.print("<!DOCTYPE ");
		output.print(documentType.getName());
		output.print(" []>");
		// NamedNodeMap entities = documentType.getEntities();
		// NamedNodeMap notations = documentType.getNotations();
	}

	public static void writeDocumentFragment(DocumentFragment documentFragment, PrintStream output) {
		throw new UnsupportedOperationException("Not yet implemented");
	}

	public static void writeElement(Element element, PrintStream output) {
		// handle the start tag + its attributes
		output.print("<" + element.getTagName());
		NamedNodeMap atts = element.getAttributes();
		for (int i = 0; i < atts.getLength(); i++) {
			output.print(" ");
			writeAttribute(((Attr) atts.item(i)), output);
		}
		// test if there are children
		NodeList children = element.getChildNodes();
		if (children.getLength() == 0) {
			// make it a self-closing tag
			output.print("/>");
		}
		// perhaps there's only one text child?
		else if (children.getLength() == 1 && children.item(0).getNodeType() == Document.TEXT_NODE) {
			// print the value of this child and the end-tag on the same line
			output.print(">" + ((Text) children.item(0)).getData());
			output.print("</" + element.getTagName() + ">");
		}
		else {
			// just a regular element, handle the children and add an endtag
			output.print(">");
			writeChildren(element, output);
			output.print("</" + element.getTagName() + ">");
		}
	}

	public static void writeAttribute(Attr attribute, PrintStream output) {
		output.print(attribute.getName() + "=\"" + attribute.getValue() + "\"");
	}

	public static void writeText(Text text, PrintStream output) {
		output.print(text.getData());
	}

	public static void writeCDATA(CDATASection cdata, PrintStream output) {
		output.print("<![CDATA[" + cdata.getData() + "]]>");
	}

	public static void writeEntity(Entity entity, PrintStream output) {
		throw new UnsupportedOperationException("not yet implemented");
	}

	public static void writeEntityReference(EntityReference entityReference, PrintStream output) {
		throw new UnsupportedOperationException("not yet implemented");
	}

	public static void writeProcessingInstruction(ProcessingInstruction instr, PrintStream output) {
		throw new UnsupportedOperationException("not yet implemented");
	}

	public static void writeComment(Comment comment, PrintStream output) {
		output.print("<!-- " + comment.getData() + " -->");
	}

	public static void writeNotation(Notation notation, PrintStream output) {
		throw new UnsupportedOperationException("not yet implemented");
	}

	public static void writeChildren(Node node, PrintStream output) {
		NodeList list = node.getChildNodes();
		for (int i = 0; i < list.getLength(); i++) {
			writeXMLNode(list.item(i), output);
		}
	}
}
